Skip to content

feat: initial SyncMmr impl#1636

Merged
drahnr merged 20 commits intonextfrom
bernhard-obsolete-syn-state-endpoint
Feb 16, 2026
Merged

feat: initial SyncMmr impl#1636
drahnr merged 20 commits intonextfrom
bernhard-obsolete-syn-state-endpoint

Conversation

@drahnr
Copy link
Contributor

@drahnr drahnr commented Feb 3, 2026

Why

SynState is too broad and complicated.

What

  1. Obsoletes SynState RPC endpoint
  2. Adds SyncChainMmr RPC endpoint

Context

Closes #1591

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request deprecates the overly broad SyncState RPC endpoint and introduces a new, more focused SyncChainMmr endpoint for syncing chain MMR data, addressing issue #1591.

Changes:

  • Deprecated the SyncState RPC endpoint in favor of more specialized endpoints
  • Added new SyncChainMmr RPC endpoint to sync chain MMR deltas within a specified block range
  • Implemented supporting infrastructure including error types, tests, and endpoint limits configuration

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
proto/proto/rpc.proto Marked SyncState as deprecated and added SyncChainMmrRequest/SyncChainMmrResponse message definitions
proto/proto/internal/store.proto Deprecated internal SyncState RPC and added SyncChainMmr RPC endpoint
crates/store/src/state/sync_state.rs Implemented sync_chain_mmr method to compute MMR deltas for given block ranges
crates/store/src/server/rpc_api.rs Added RPC handler for SyncChainMmr with parameter validation and error handling
crates/store/src/errors.rs Introduced SyncChainMmrError enum for request validation errors
crates/rpc/src/server/api.rs Added passthrough implementation and endpoint limit configuration for SyncChainMmr
crates/rpc/src/tests.rs Added test for SyncChainMmr endpoint and verified it appears in limits response
crates/rpc/Cargo.toml Enabled rocksdb feature for miden-node-store in dev-dependencies to support integration tests
crates/proto/src/generated/store.rs Auto-generated gRPC client/server code for the new endpoint
crates/proto/src/generated/rpc.rs Auto-generated gRPC client/server code for the new endpoint

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@drahnr drahnr marked this pull request as ready for review February 6, 2026 13:23
@drahnr drahnr force-pushed the bernhard-obsolete-syn-state-endpoint branch from c83c375 to 3e8fdd2 Compare February 9, 2026 10:59
Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loving the line delta :)

Comment on lines 52 to 53
// - Mmr::get_delta is inclusive, whereas the sync request block_from is defined to be
// exclusive, so the from_forest has to be adjusted with a +1.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry about this definition. Users will expect (I think) MMR to return the values as per the block header number for that block, and get the mmr as at that block. Unless I'm misunderstanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to change, kept it in-line with the previous SynState.

From a practicality perspective, when calling I have my chain-tip and want everything starting from there, so I think the common call is to do local_chain_tip.. as the request. The response I'd expect is { (local_chain_tip+1)..=(local_chain_tip+PAGE_SIZE), Mmr for <<that range }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thing is then you can't sync from genesis though? Though I guess you have to fetch genesis header and build your own from there which you need to do to compare the hashes in any case

Copy link
Contributor Author

@drahnr drahnr Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, @igamigo is that an issue/a lot of extra work? We can also be redundent and return including the local_chain_tip (client perspective).


To expand a bit on expected usage:

Assuming there are more entries in the MMR than PAGE_SIZE, the next call would be:
local_chain_tip+1+PAGE_SIZE.. yielding (local_chain_tip+1+PAGE_SIZE)..=(local_chain_tip+PAGE_SIZE*2), Mmr for <<that range } where local_chain_tip+PAGE_SIZE is part of the pagination info returned.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can return the response here starting on local_chain_tip, not without making it more inconvenient to apply the changes within the client anyway. Basically, we need to store peaks for a specific block (e.g, for block number 10 we want to keep the peaks of the MMR with 11 leaves). For this, when syncing, we basically apply the MmrDelta (which gets your MMR to block number 9), then apply the individual block header (number 10), and get the peaks to store alongside the block header. If on a subsequent response we got the MmrDelta starting block number 10 (whereas currently we would receive it starting block 11); we'd need to "rolllback" the MMR and apply the delta accordingly.
TLDR, I believe the current way is fine and overlapping the delta with the response/request tips may complicate things.

@drahnr drahnr force-pushed the bernhard-obsolete-syn-state-endpoint branch from 7dc8b4e to 76c4ecd Compare February 10, 2026 15:58
@drahnr drahnr requested a review from igamigo February 13, 2026 12:36
Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM in terms of implementation! Left a couple of questions but I don't think they are blocking

&self,
request: Request<proto::rpc::SyncChainMmrRequest>,
) -> Result<Response<proto::rpc::SyncChainMmrResponse>, Status> {
const MAX_BLOCKS: u32 = 1000;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this limit based on anything? I don't think it meaningfully caps the response size. It would probably be less overhead in terms of walking the MMR but I wonder if this is below what would be sensible since it should be logarithmic.

Copy link
Contributor Author

@drahnr drahnr Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always up it to u32::MAX (as limited by PageInfo response type). The size of Mmr is a set of roots + depth of tree (so yes, log2(n)) to be transferred which should be fine for u32::MAX (or beyond if we change the representation of PageInfo).

@drahnr drahnr merged commit dea85f7 into next Feb 16, 2026
20 checks passed
@drahnr drahnr deleted the bernhard-obsolete-syn-state-endpoint branch February 16, 2026 11:58
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this has already been merged, but I left some comments inline and it would be god to address them in a follow-up PR.

// part of hashes. Thus, returned data contains excessive notes, client can make
// additional filtering of that data on its side.
rpc SyncState(SyncStateRequest) returns (SyncStateResponse) {}
rpc SyncChainMmr(SyncChainMmrRequest) returns (SyncChainMmrResponse) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some basic doc comments here (similar to how we do it for other endpoints in this file).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a subsection for SynChainMmr endpoint here.

Comment on lines 140 to 147
"endpoints": {
"CheckNullifiers": { "parameters": { "nullifier": 1000 } },
"SyncNullifiers": { "parameters": { "nullifier": 1000 } },
"SyncState": { "parameters": { "account_id": 1000, "note_tag": 1000 } },
"SyncTransactions": { "parameters": { "account_id": 1000 } },
"SyncAccountVault": { "parameters": { "account_id": 1000 } },
"SyncAccountStorageMaps": { "parameters": { "account_id": 1000 } },
"SyncNotes": { "parameters": { "note_tag": 1000 } },
"GetNotesById": { "parameters": { "note_id": 100 } }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't SyncChainMmr also be in this list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it should, since it's paginated over the block range, so whatever requests a piece is returned with pagination info required to resume

Comment on lines +123 to +127
/// Used for the following RPC endpoints:
/// * `sync_chain_mmr`
///
/// Capped at 1000 blocks to keep MMR deltas within the 4 MB payload budget.
pub struct QueryParamBlockRangeLimit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the computation here correct? Feels like we should be able to query for way more blocks before before we hit the 4 MB limit (and since we are not sending authentication paths, I'm actually not sure we'll ever hit the limit).

Also, 1000 bocks is current about ~1 hour. If we do keep this limit, we should bump it up significantly so that a single request could cover multiple months.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accidentally made it in. It's not used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my other comment: we should probably add a section for SyncChainMmr here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to some other comments: this file should probably have a section on SyncChainMmr endpoint.

Comment on lines +195 to +201
async fn sync_chain_mmr(
&self,
request: Request<proto::rpc::SyncStateRequest>,
) -> Result<Response<proto::rpc::SyncStateResponse>, Status> {
request: Request<proto::rpc::SyncChainMmrRequest>,
) -> Result<Response<proto::rpc::SyncChainMmrResponse>, Status> {
debug!(target: COMPONENT, request = ?request.get_ref());

check::<QueryParamAccountIdLimit>(request.get_ref().account_ids.len())?;
check::<QueryParamNoteTagLimit>(request.get_ref().note_tags.len())?;

self.store.clone().sync_state(request).await
self.store.clone().sync_chain_mmr(request).await
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though we define a limit for block range, it doesn't seem like we enforce it here (unless I'm missing something). But then again, I'm not sure we actually need this limit.

Copy link
Contributor Author

@drahnr drahnr Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block range limit was accidentally committed and doesn't make sense for paged request

Comment on lines +161 to +162
// TODO find a reasonable upper boundary
const MAX_BLOCKS: u32 = 1 << 20;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to some other comments - I'm not sure there needs to be an upper bound.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no upper bound, even pagination, then we should remove pagination altogether.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main intent for specifying the block range was so that the client could request to sync to a specific block height (rather than the chain tip). Basically, the client needs to be able to say: I want chain MMR delta starting from block $x$ and up to block $y$. But this is technically not pagination.

cc @igamigo in case I'm off on this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's mainly it. I think with the new flow we'd get relevant block headers through SyncNotes and then we can SyncMmr as needed.

@drahnr
Copy link
Contributor Author

drahnr commented Feb 17, 2026

Created #1682 for follow-ups

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove the SynState endpoint

4 participants

Comments